package dynamic.ibatis.session;


import dynamic.ibatis.binding.DMapperRegistry;
import dynamic.ibatis.builder.DCacheRefResolver;
import dynamic.ibatis.builder.DResultMapResolver;
import dynamic.ibatis.builder.annotation.DMethodResolver;
import dynamic.ibatis.builder.xml.DXMLStatementBuilder;
import dynamic.ibatis.executor.*;
import dynamic.ibatis.executor.keygen.DKeyGenerator;
import dynamic.ibatis.executor.loader.JavassistProxyFactory;
import dynamic.ibatis.executor.loader.ProxyFactory;
import dynamic.ibatis.executor.parameter.DParameterHandler;
import dynamic.ibatis.executor.resultset.DefaultResultSetHandler;
import dynamic.ibatis.executor.statement.DRoutingStatementHandler;
import dynamic.ibatis.executor.statement.StatementHandler;
import dynamic.ibatis.mapping.DBoundSql;
import dynamic.ibatis.mapping.DMappedStatement;
import dynamic.ibatis.mapping.DParameterMap;
import dynamic.ibatis.mapping.DResultMap;
import dynamic.ibatis.reflection.DefaultReflectorFactory;
import dynamic.ibatis.reflection.MetaObject;
import dynamic.ibatis.reflection.factory.DefaultObjectFactory;
import dynamic.ibatis.reflection.factory.ObjectFactory;
import dynamic.ibatis.reflection.wrapper.ObjectWrapperFactory;
import dynamic.ibatis.scripting.DLanguageDriver;
import dynamic.ibatis.scripting.DLanguageDriverRegistry;
import dynamic.ibatis.scripting.defaults.DRawLanguageDriver;
import dynamic.ibatis.scripting.xmltags.DXMLLanguageDriver;
import dynamic.ibatis.type.DTypeHandlerRegistry;
import dynamic.metadata.EntityClass;
import org.apache.ibatis.builder.IncompleteElementException;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.decorators.FifoCache;
import org.apache.ibatis.cache.decorators.LruCache;
import org.apache.ibatis.cache.decorators.SoftCache;
import org.apache.ibatis.cache.decorators.WeakCache;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.apache.ibatis.datasource.jndi.JndiDataSourceFactory;
import org.apache.ibatis.datasource.pooled.PooledDataSourceFactory;
import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;

import org.apache.ibatis.executor.loader.cglib.CglibProxyFactory;

import org.apache.ibatis.executor.resultset.ResultSetHandler;

import org.apache.ibatis.io.VFS;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl;
import org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl;
import org.apache.ibatis.logging.log4j.Log4jImpl;
import org.apache.ibatis.logging.log4j2.Log4j2Impl;
import org.apache.ibatis.logging.nologging.NoLoggingImpl;
import org.apache.ibatis.logging.slf4j.Slf4jImpl;
import org.apache.ibatis.logging.stdout.StdOutImpl;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultSetType;
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.InterceptorChain;



import dynamic.ibatis.reflection.ReflectorFactory;
import dynamic.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;


import org.apache.ibatis.scripting.defaults.RawLanguageDriver;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.*;
import org.apache.ibatis.transaction.Transaction;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.apache.ibatis.transaction.managed.ManagedTransactionFactory;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeAliasRegistry;
import org.apache.ibatis.type.TypeHandler;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;

/**
 * @author Clinton Begin
 */
public class DConfiguration {

    protected Environment environment;

    protected boolean safeRowBoundsEnabled;
    protected boolean safeResultHandlerEnabled = true;
    protected boolean mapUnderscoreToCamelCase;
    protected boolean aggressiveLazyLoading;
    protected boolean multipleResultSetsEnabled = true;
    protected boolean useGeneratedKeys;
    protected boolean useColumnLabel = true;
    protected boolean cacheEnabled = true;
    protected boolean callSettersOnNulls;
    protected boolean useActualParamName = true;
    protected boolean returnInstanceForEmptyRow;
    protected boolean shrinkWhitespacesInSql;
    protected boolean nullableOnForEach;
    protected boolean argNameBasedConstructorAutoMapping;

    protected String logPrefix;
    protected Class<? extends Log> logImpl;
    protected Class<? extends VFS> vfsImpl;
    protected Class<?> defaultSqlProviderType;
    protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
    protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
    protected Set<String> lazyLoadTriggerMethods = new HashSet<>(
            Arrays.asList("equals", "clone", "hashCode", "toString"));
    protected Integer defaultStatementTimeout;
    protected Integer defaultFetchSize;
    protected ResultSetType defaultResultSetType;
    protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
    protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
    protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;

    protected Properties variables = new Properties();
    protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
    protected ObjectFactory objectFactory = new DefaultObjectFactory();
    protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();

    protected boolean lazyLoadingEnabled;
    protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL

    protected String databaseId;
    /**
     * Configuration factory class. Used to create Configuration for loading deserialized unread properties.
     *
     * @see <a href='https://github.com/mybatis/old-google-code-issues/issues/300'>Issue 300 (google code)</a>
     */
    protected Class<?> configurationFactory;

    protected final DMapperRegistry mapperRegistry = new DMapperRegistry(this);
    protected final InterceptorChain interceptorChain = new InterceptorChain();
    protected final DTypeHandlerRegistry typeHandlerRegistry = new DTypeHandlerRegistry(this);
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
    protected final DLanguageDriverRegistry languageRegistry = new DLanguageDriverRegistry();

    protected final Map<String, DMappedStatement> mappedStatements = new StrictMap<DMappedStatement>(
            "Mapped Statements collection")
            .conflictMessageProducer((savedValue, targetValue) -> ". please check " + savedValue.getResource() + " and "
                    + targetValue.getResource());
    protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
    protected final Map<String, DResultMap> resultMaps = new StrictMap<>("Result Maps collection");
    protected final Map<String, DParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
    protected final Map<String, DKeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");

    protected final Map<String,Set<EntityClass>> loadedResources = new HashMap<>();
    protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");

    protected final Collection<DXMLStatementBuilder> incompleteStatements = new LinkedList<>();
    protected final Collection<DCacheRefResolver> incompleteCacheRefs = new LinkedList<>();
    protected final Collection<DResultMapResolver> incompleteResultMaps = new LinkedList<>();
    protected final Collection<DMethodResolver> incompleteMethods = new LinkedList<>();

    /*
     * A map holds cache-ref relationship. The key is the namespace that references a cache bound to another namespace and
     * the value is the namespace which the actual cache is bound to.
     */
    protected final Map<String, String> cacheRefMap = new HashMap<>();

    public DConfiguration(Environment environment) {
        this();
        this.environment = environment;
    }

    public DConfiguration() {
        typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
        typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

        typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
        typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
        typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);

        typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
        typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
        typeAliasRegistry.registerAlias("LRU", LruCache.class);
        typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
        typeAliasRegistry.registerAlias("WEAK", WeakCache.class);

        typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);

        typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
        typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);

        typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
        typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
        typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
        typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
        typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
        typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
        typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);

        typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
        typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);

        languageRegistry.setDefaultDriverClass(DXMLLanguageDriver.class);
        languageRegistry.register(DRawLanguageDriver.class);
    }

    public String getLogPrefix() {
        return logPrefix;
    }

    public void setLogPrefix(String logPrefix) {
        this.logPrefix = logPrefix;
    }

    public Class<? extends Log> getLogImpl() {
        return logImpl;
    }

    public void setLogImpl(Class<? extends Log> logImpl) {
        if (logImpl != null) {
            this.logImpl = logImpl;
            LogFactory.useCustomLogging(this.logImpl);
        }
    }

    public Class<? extends VFS> getVfsImpl() {
        return this.vfsImpl;
    }

    public void setVfsImpl(Class<? extends VFS> vfsImpl) {
        if (vfsImpl != null) {
            this.vfsImpl = vfsImpl;
            VFS.addImplClass(this.vfsImpl);
        }
    }

    /**
     * Gets an applying type when omit a type on sql provider annotation(e.g.
     * {@link org.apache.ibatis.annotations.SelectProvider}).
     *
     * @return the default type for sql provider annotation
     *
     * @since 3.5.6
     */
    public Class<?> getDefaultSqlProviderType() {
        return defaultSqlProviderType;
    }

    /**
     * Sets an applying type when omit a type on sql provider annotation(e.g.
     * {@link org.apache.ibatis.annotations.SelectProvider}).
     *
     * @param defaultSqlProviderType
     *          the default type for sql provider annotation
     *
     * @since 3.5.6
     */
    public void setDefaultSqlProviderType(Class<?> defaultSqlProviderType) {
        this.defaultSqlProviderType = defaultSqlProviderType;
    }

    public boolean isCallSettersOnNulls() {
        return callSettersOnNulls;
    }

    public void setCallSettersOnNulls(boolean callSettersOnNulls) {
        this.callSettersOnNulls = callSettersOnNulls;
    }

    public boolean isUseActualParamName() {
        return useActualParamName;
    }

    public void setUseActualParamName(boolean useActualParamName) {
        this.useActualParamName = useActualParamName;
    }

    public boolean isReturnInstanceForEmptyRow() {
        return returnInstanceForEmptyRow;
    }

    public void setReturnInstanceForEmptyRow(boolean returnEmptyInstance) {
        this.returnInstanceForEmptyRow = returnEmptyInstance;
    }

    public boolean isShrinkWhitespacesInSql() {
        return shrinkWhitespacesInSql;
    }

    public void setShrinkWhitespacesInSql(boolean shrinkWhitespacesInSql) {
        this.shrinkWhitespacesInSql = shrinkWhitespacesInSql;
    }

    /**
     * Sets the default value of 'nullable' attribute on 'foreach' tag.
     *
     * @param nullableOnForEach
     *          If nullable, set to {@code true}
     *
     * @since 3.5.9
     */
    public void setNullableOnForEach(boolean nullableOnForEach) {
        this.nullableOnForEach = nullableOnForEach;
    }

    /**
     * Returns the default value of 'nullable' attribute on 'foreach' tag.
     * <p>
     * Default is {@code false}.
     *
     * @return If nullable, set to {@code true}
     *
     * @since 3.5.9
     */
    public boolean isNullableOnForEach() {
        return nullableOnForEach;
    }

    public boolean isArgNameBasedConstructorAutoMapping() {
        return argNameBasedConstructorAutoMapping;
    }

    public void setArgNameBasedConstructorAutoMapping(boolean argNameBasedConstructorAutoMapping) {
        this.argNameBasedConstructorAutoMapping = argNameBasedConstructorAutoMapping;
    }

    public String getDatabaseId() {
        return databaseId;
    }

    public void setDatabaseId(String databaseId) {
        this.databaseId = databaseId;
    }

    public Class<?> getConfigurationFactory() {
        return configurationFactory;
    }

    public void setConfigurationFactory(Class<?> configurationFactory) {
        this.configurationFactory = configurationFactory;
    }

    public boolean isSafeResultHandlerEnabled() {
        return safeResultHandlerEnabled;
    }

    public void setSafeResultHandlerEnabled(boolean safeResultHandlerEnabled) {
        this.safeResultHandlerEnabled = safeResultHandlerEnabled;
    }

    public boolean isSafeRowBoundsEnabled() {
        return safeRowBoundsEnabled;
    }

    public void setSafeRowBoundsEnabled(boolean safeRowBoundsEnabled) {
        this.safeRowBoundsEnabled = safeRowBoundsEnabled;
    }

    public boolean isMapUnderscoreToCamelCase() {
        return mapUnderscoreToCamelCase;
    }

    public void setMapUnderscoreToCamelCase(boolean mapUnderscoreToCamelCase) {
        this.mapUnderscoreToCamelCase = mapUnderscoreToCamelCase;
    }

    public void addLoadedResource(String resource,EntityClass entityClass) {
        if(!loadedResources.containsKey(resource)){
            loadedResources.put(resource,new HashSet<EntityClass>());
        }
        loadedResources.get(resource).add(entityClass);
    }

    public boolean isResourceLoaded(String resource,EntityClass entityClass) {
        return loadedResources.containsKey(resource)&&loadedResources.get(resource).contains(entityClass);
    }

    public Environment getEnvironment() {
        return environment;
    }

    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    public AutoMappingBehavior getAutoMappingBehavior() {
        return autoMappingBehavior;
    }

    public void setAutoMappingBehavior(AutoMappingBehavior autoMappingBehavior) {
        this.autoMappingBehavior = autoMappingBehavior;
    }

    /**
     * Gets the auto mapping unknown column behavior.
     *
     * @return the auto mapping unknown column behavior
     *
     * @since 3.4.0
     */
    public AutoMappingUnknownColumnBehavior getAutoMappingUnknownColumnBehavior() {
        return autoMappingUnknownColumnBehavior;
    }

    /**
     * Sets the auto mapping unknown column behavior.
     *
     * @param autoMappingUnknownColumnBehavior
     *          the new auto mapping unknown column behavior
     *
     * @since 3.4.0
     */
    public void setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior) {
        this.autoMappingUnknownColumnBehavior = autoMappingUnknownColumnBehavior;
    }

    public boolean isLazyLoadingEnabled() {
        return lazyLoadingEnabled;
    }

    public void setLazyLoadingEnabled(boolean lazyLoadingEnabled) {
        this.lazyLoadingEnabled = lazyLoadingEnabled;
    }

    public ProxyFactory  getProxyFactory() {
        return proxyFactory;
    }

    public void setProxyFactory(ProxyFactory proxyFactory) {
        if (proxyFactory == null) {
            proxyFactory = new JavassistProxyFactory();
        }
        this.proxyFactory = proxyFactory;
    }

    public boolean isAggressiveLazyLoading() {
        return aggressiveLazyLoading;
    }

    public void setAggressiveLazyLoading(boolean aggressiveLazyLoading) {
        this.aggressiveLazyLoading = aggressiveLazyLoading;
    }

    public boolean isMultipleResultSetsEnabled() {
        return multipleResultSetsEnabled;
    }

    public void setMultipleResultSetsEnabled(boolean multipleResultSetsEnabled) {
        this.multipleResultSetsEnabled = multipleResultSetsEnabled;
    }

    public Set<String> getLazyLoadTriggerMethods() {
        return lazyLoadTriggerMethods;
    }

    public void setLazyLoadTriggerMethods(Set<String> lazyLoadTriggerMethods) {
        this.lazyLoadTriggerMethods = lazyLoadTriggerMethods;
    }

    public boolean isUseGeneratedKeys() {
        return useGeneratedKeys;
    }

    public void setUseGeneratedKeys(boolean useGeneratedKeys) {
        this.useGeneratedKeys = useGeneratedKeys;
    }

    public ExecutorType getDefaultExecutorType() {
        return defaultExecutorType;
    }

    public void setDefaultExecutorType(ExecutorType defaultExecutorType) {
        this.defaultExecutorType = defaultExecutorType;
    }

    public boolean isCacheEnabled() {
        return cacheEnabled;
    }

    public void setCacheEnabled(boolean cacheEnabled) {
        this.cacheEnabled = cacheEnabled;
    }

    public Integer getDefaultStatementTimeout() {
        return defaultStatementTimeout;
    }

    public void setDefaultStatementTimeout(Integer defaultStatementTimeout) {
        this.defaultStatementTimeout = defaultStatementTimeout;
    }

    /**
     * Gets the default fetch size.
     *
     * @return the default fetch size
     *
     * @since 3.3.0
     */
    public Integer getDefaultFetchSize() {
        return defaultFetchSize;
    }

    /**
     * Sets the default fetch size.
     *
     * @param defaultFetchSize
     *          the new default fetch size
     *
     * @since 3.3.0
     */
    public void setDefaultFetchSize(Integer defaultFetchSize) {
        this.defaultFetchSize = defaultFetchSize;
    }

    /**
     * Gets the default result set type.
     *
     * @return the default result set type
     *
     * @since 3.5.2
     */
    public ResultSetType getDefaultResultSetType() {
        return defaultResultSetType;
    }

    /**
     * Sets the default result set type.
     *
     * @param defaultResultSetType
     *          the new default result set type
     *
     * @since 3.5.2
     */
    public void setDefaultResultSetType(ResultSetType defaultResultSetType) {
        this.defaultResultSetType = defaultResultSetType;
    }

    public boolean isUseColumnLabel() {
        return useColumnLabel;
    }

    public void setUseColumnLabel(boolean useColumnLabel) {
        this.useColumnLabel = useColumnLabel;
    }

    public LocalCacheScope getLocalCacheScope() {
        return localCacheScope;
    }

    public void setLocalCacheScope(LocalCacheScope localCacheScope) {
        this.localCacheScope = localCacheScope;
    }

    public JdbcType getJdbcTypeForNull() {
        return jdbcTypeForNull;
    }

    public void setJdbcTypeForNull(JdbcType jdbcTypeForNull) {
        this.jdbcTypeForNull = jdbcTypeForNull;
    }

    public Properties getVariables() {
        return variables;
    }

    public void setVariables(Properties variables) {
        this.variables = variables;
    }

    public DTypeHandlerRegistry getTypeHandlerRegistry() {
        return typeHandlerRegistry;
    }

    /**
     * Set a default {@link TypeHandler} class for {@link Enum}. A default {@link TypeHandler} is
     * {@link org.apache.ibatis.type.EnumTypeHandler}.
     *
     * @param typeHandler
     *          a type handler class for {@link Enum}
     *
     * @since 3.4.5
     */
    public void setDefaultEnumTypeHandler(Class<? extends TypeHandler> typeHandler) {
        if (typeHandler != null) {
            getTypeHandlerRegistry().setDefaultEnumTypeHandler(typeHandler);
        }
    }

    public TypeAliasRegistry getTypeAliasRegistry() {
        return typeAliasRegistry;
    }

    /**
     * Gets the mapper registry.
     *
     * @return the mapper registry
     *
     * @since 3.2.2
     */
    public DMapperRegistry getMapperRegistry() {
        return mapperRegistry;
    }

    public ReflectorFactory getReflectorFactory() {
        return reflectorFactory;
    }

    public void setReflectorFactory(ReflectorFactory reflectorFactory) {
        this.reflectorFactory = reflectorFactory;
    }

    public ObjectFactory getObjectFactory() {
        return objectFactory;
    }

    public void setObjectFactory(ObjectFactory objectFactory) {
        this.objectFactory = objectFactory;
    }

    public ObjectWrapperFactory getObjectWrapperFactory() {
        return objectWrapperFactory;
    }

    public void setObjectWrapperFactory(ObjectWrapperFactory objectWrapperFactory) {
        this.objectWrapperFactory = objectWrapperFactory;
    }

    /**
     * Gets the interceptors.
     *
     * @return the interceptors
     *
     * @since 3.2.2
     */
    public List<Interceptor> getInterceptors() {
        return interceptorChain.getInterceptors();
    }

    public DLanguageDriverRegistry getLanguageRegistry() {
        return languageRegistry;
    }

    public void setDefaultScriptingLanguage(Class<? extends DLanguageDriver> driver) {
        if (driver == null) {
            driver = DXMLLanguageDriver.class;
        }
        getLanguageRegistry().setDefaultDriverClass(driver);
    }

    public DLanguageDriver getDefaultScriptingLanguageInstance() {
        return languageRegistry.getDefaultDriver();
    }

    /**
     * Gets the language driver.
     *
     * @param langClass
     *          the lang class
     *
     * @return the language driver
     *
     * @since 3.5.1
     */
    public DLanguageDriver getLanguageDriver(Class<? extends DLanguageDriver> langClass) {
        if (langClass == null) {
            return languageRegistry.getDefaultDriver();
        }
        languageRegistry.register(langClass);
        return languageRegistry.getDriver(langClass);
    }

    /**
     * Gets the default scripting language instance.
     *
     * @return the default scripting language instance
     *
     * @deprecated Use {@link #getDefaultScriptingLanguageInstance()}
     */
    @Deprecated
    public DLanguageDriver getDefaultScriptingLanuageInstance() {
        return getDefaultScriptingLanguageInstance();
    }

    public MetaObject newMetaObject(Object object) {
        return MetaObject.forObject(object, objectFactory, objectWrapperFactory, reflectorFactory);
    }

    public DParameterHandler newParameterHandler(DMappedStatement mappedStatement, Object parameterObject,
                                                DBoundSql boundSql) {
        DParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,
                parameterObject, boundSql);
        return (DParameterHandler) interceptorChain.pluginAll(parameterHandler);
    }

    public ResultSetHandler newResultSetHandler(DExecutor executor, DMappedStatement mappedStatement, RowBounds rowBounds,
                                                DParameterHandler parameterHandler, ResultHandler resultHandler, DBoundSql boundSql) {
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler,
                resultHandler, boundSql, rowBounds);
        return (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    }

    public StatementHandler newStatementHandler(DExecutor executor, DMappedStatement mappedStatement,
                                                Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, DBoundSql boundSql) {
        StatementHandler statementHandler = new DRoutingStatementHandler(executor, mappedStatement, parameterObject,
                rowBounds, resultHandler, boundSql);
        return (StatementHandler) interceptorChain.pluginAll(statementHandler);
    }

    public DExecutor newExecutor(Transaction transaction) {
        return newExecutor(transaction, defaultExecutorType);
    }

    public DExecutor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        DExecutor executor;
        if (ExecutorType.BATCH == executorType) {
            executor = new DBatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new DReuseExecutor(this, transaction);
        } else {
            executor = new DSimpleExecutor(this, transaction);
        }
        if (cacheEnabled) {
            executor = new DCachingExecutor(executor);
        }
        return (DExecutor) interceptorChain.pluginAll(executor);
    }

    public void addKeyGenerator(String id, DKeyGenerator keyGenerator) {
        keyGenerators.put(id, keyGenerator);
    }

    public Collection<String> getKeyGeneratorNames() {
        return keyGenerators.keySet();
    }

    public Collection<DKeyGenerator> getKeyGenerators() {
        return keyGenerators.values();
    }

    public DKeyGenerator getKeyGenerator(String id) {
        return keyGenerators.get(id);
    }

    public boolean hasKeyGenerator(String id) {
        return keyGenerators.containsKey(id);
    }

    public void addCache(Cache cache) {
        caches.put(cache.getId(), cache);
    }

    public Collection<String> getCacheNames() {
        return caches.keySet();
    }

    public Collection<Cache> getCaches() {
        return caches.values();
    }

    public Cache getCache(String id) {
        return caches.get(id);
    }

    public boolean hasCache(String id) {
        return caches.containsKey(id);
    }

    public void addResultMap(DResultMap rm) {
        resultMaps.put(rm.getId(), rm);
        checkLocallyForDiscriminatedNestedResultMaps(rm);
        checkGloballyForDiscriminatedNestedResultMaps(rm);
    }

    public Collection<String> getResultMapNames() {
        return resultMaps.keySet();
    }

    public Collection<DResultMap> getResultMaps() {
        return resultMaps.values();
    }

    public DResultMap getResultMap(String id) {
        return resultMaps.get(id);
    }

    public boolean hasResultMap(String id) {
        return resultMaps.containsKey(id);
    }

    public void addParameterMap(DParameterMap pm) {
        parameterMaps.put(pm.getId(), pm);
    }

    public Collection<String> getParameterMapNames() {
        return parameterMaps.keySet();
    }

    public Collection<DParameterMap> getParameterMaps() {
        return parameterMaps.values();
    }

    public DParameterMap getParameterMap(String id) {
        return parameterMaps.get(id);
    }

    public boolean hasParameterMap(String id) {
        return parameterMaps.containsKey(id);
    }

    public void addMappedStatement(DMappedStatement ms) {
        mappedStatements.put(ms.getId(), ms);
    }

    public Collection<String> getMappedStatementNames() {
        buildAllStatements();
        return mappedStatements.keySet();
    }

    public Collection<DMappedStatement> getMappedStatements() {
        buildAllStatements();
        return mappedStatements.values();
    }

    public Collection<DXMLStatementBuilder> getIncompleteStatements() {
        return incompleteStatements;
    }

    public void addIncompleteStatement(DXMLStatementBuilder incompleteStatement) {
        incompleteStatements.add(incompleteStatement);
    }

    public Collection<DCacheRefResolver> getIncompleteCacheRefs() {
        return incompleteCacheRefs;
    }

    public void addIncompleteCacheRef(DCacheRefResolver incompleteCacheRef) {
        incompleteCacheRefs.add(incompleteCacheRef);
    }

    public Collection<DResultMapResolver> getIncompleteResultMaps() {
        return incompleteResultMaps;
    }

    public void addIncompleteResultMap(DResultMapResolver resultMapResolver) {
        incompleteResultMaps.add(resultMapResolver);
    }

    public void addIncompleteMethod(DMethodResolver builder) {
        incompleteMethods.add(builder);
    }

    public Collection<DMethodResolver> getIncompleteMethods() {
        return incompleteMethods;
    }

    public DMappedStatement getMappedStatement(String id) {
        return this.getMappedStatement(id, true);
    }

    public DMappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
        if (validateIncompleteStatements) {
            buildAllStatements();
        }
        return mappedStatements.get(id);
    }

    public Map<String, XNode> getSqlFragments() {
        return sqlFragments;
    }

    public void addInterceptor(Interceptor interceptor) {
        interceptorChain.addInterceptor(interceptor);
    }

//    public void addMappers(String packageName, Class<?> superType) {
//        mapperRegistry.addMappers(packageName, superType);
//    }

//    public void addMappers(String packageName) {
//        mapperRegistry.addMappers(packageName);
//    }

    public <T> void addMapper(Class<T> type, EntityClass entityClass) {
        mapperRegistry.addMapper(type,entityClass);
    }

    public <T> T getMapper(Class<T> type,EntityClass entityClass, DSqlSession sqlSession) {
        return mapperRegistry.getMapper(type, entityClass, sqlSession);
    }

    public boolean hasMapper(Class<?> type,EntityClass entityClass) {
        return mapperRegistry.hasMapper(type,entityClass);
    }

    public boolean hasStatement(String statementName) {
        return hasStatement(statementName, true);
    }

    public boolean hasStatement(String statementName, boolean validateIncompleteStatements) {
        if (validateIncompleteStatements) {
            buildAllStatements();
        }
        return mappedStatements.containsKey(statementName);
    }

    public void addCacheRef(String namespace, String referencedNamespace) {
        cacheRefMap.put(namespace, referencedNamespace);
    }

    /*
     * Parses all the unprocessed statement nodes in the cache. It is recommended to call this method once all the mappers
     * are added as it provides fail-fast statement validation.
     */
    protected void buildAllStatements() {
        parsePendingResultMaps();
        if (!incompleteCacheRefs.isEmpty()) {
            synchronized (incompleteCacheRefs) {
                incompleteCacheRefs.removeIf(x -> x.resolveCacheRef() != null);
            }
        }
        if (!incompleteStatements.isEmpty()) {
            synchronized (incompleteStatements) {
                incompleteStatements.removeIf(x -> {
                    x.parseStatementNode();
                    return true;
                });
            }
        }
        if (!incompleteMethods.isEmpty()) {
            synchronized (incompleteMethods) {
                incompleteMethods.removeIf(x -> {
                    x.resolve();
                    return true;
                });
            }
        }
    }

    private void parsePendingResultMaps() {
        if (incompleteResultMaps.isEmpty()) {
            return;
        }
        synchronized (incompleteResultMaps) {
            boolean resolved;
            IncompleteElementException ex = null;
            do {
                resolved = false;
                Iterator<DResultMapResolver> iterator = incompleteResultMaps.iterator();
                while (iterator.hasNext()) {
                    try {
                        iterator.next().resolve();
                        iterator.remove();
                        resolved = true;
                    } catch (IncompleteElementException e) {
                        ex = e;
                    }
                }
            } while (resolved);
            if (!incompleteResultMaps.isEmpty() && ex != null) {
                // At least one result map is unresolvable.
                throw ex;
            }
        }
    }

    /**
     * Extracts namespace from fully qualified statement id.
     *
     * @param statementId
     *          the statement id
     *
     * @return namespace or null when id does not contain period.
     */
    protected String extractNamespace(String statementId) {
        int lastPeriod = statementId.lastIndexOf('.');
        return lastPeriod > 0 ? statementId.substring(0, lastPeriod) : null;
    }

    // Slow but a one time cost. A better solution is welcome.
    protected void checkGloballyForDiscriminatedNestedResultMaps(DResultMap rm) {
        if (rm.hasNestedResultMaps()) {
            final String resultMapId = rm.getId();
            for (Object resultMapObject : resultMaps.values()) {
                if (resultMapObject instanceof ResultMap) {
                    ResultMap entryResultMap = (ResultMap) resultMapObject;
                    if (!entryResultMap.hasNestedResultMaps() && entryResultMap.getDiscriminator() != null) {
                        Collection<String> discriminatedResultMapNames = entryResultMap.getDiscriminator().getDiscriminatorMap()
                                .values();
                        if (discriminatedResultMapNames.contains(resultMapId)) {
                            entryResultMap.forceNestedResultMaps();
                        }
                    }
                }
            }
        }
    }

    // Slow but a one time cost. A better solution is welcome.
    protected void checkLocallyForDiscriminatedNestedResultMaps(DResultMap rm) {
        if (!rm.hasNestedResultMaps() && rm.getDiscriminator() != null) {
            for (String discriminatedResultMapName : rm.getDiscriminator().getDiscriminatorMap().values()) {
                if (hasResultMap(discriminatedResultMapName)) {
                    DResultMap discriminatedResultMap = resultMaps.get(discriminatedResultMapName);
                    if (discriminatedResultMap.hasNestedResultMaps()) {
                        rm.forceNestedResultMaps();
                        break;
                    }
                }
            }
        }
    }

    protected static class StrictMap<V> extends ConcurrentHashMap<String, V> {

        private static final long serialVersionUID = -4950446264854982944L;
        private final String name;
        private BiFunction<V, V, String> conflictMessageProducer;

        public StrictMap(String name, int initialCapacity, float loadFactor) {
            super(initialCapacity, loadFactor);
            this.name = name;
        }

        public StrictMap(String name, int initialCapacity) {
            super(initialCapacity);
            this.name = name;
        }

        public StrictMap(String name) {
            this.name = name;
        }

        public StrictMap(String name, Map<String, ? extends V> m) {
            super(m);
            this.name = name;
        }

        /**
         * Assign a function for producing a conflict error message when contains value with the same key.
         * <p>
         * function arguments are 1st is saved value and 2nd is target value.
         *
         * @param conflictMessageProducer
         *          A function for producing a conflict error message
         *
         * @return a conflict error message
         *
         * @since 3.5.0
         */
        public StrictMap<V> conflictMessageProducer(BiFunction<V, V, String> conflictMessageProducer) {
            this.conflictMessageProducer = conflictMessageProducer;
            return this;
        }

        @Override
        @SuppressWarnings("unchecked")
        public V put(String key, V value) {
            if (containsKey(key)) {
                throw new IllegalArgumentException(name + " already contains value for " + key
                        + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
            }
            if (key.contains(".")) {
                final String shortKey = getShortName(key);
                if (super.get(shortKey) == null) {
                    super.put(shortKey, value);
                } else {
                    super.put(shortKey, (V) new StrictMap.Ambiguity(shortKey));
                }
            }
            return super.put(key, value);
        }

        @Override
        public boolean containsKey(Object key) {
            if (key == null) {
                return false;
            }

            return super.get(key) != null;
        }

        @Override
        public V get(Object key) {
            V value = super.get(key);
            if (value == null) {
                throw new IllegalArgumentException(name + " does not contain value for " + key);
            }
            if (value instanceof StrictMap.Ambiguity) {
                throw new IllegalArgumentException(((StrictMap.Ambiguity) value).getSubject() + " is ambiguous in " + name
                        + " (try using the full name including the namespace, or rename one of the entries)");
            }
            return value;
        }

        protected static class Ambiguity {
            private final String subject;

            public Ambiguity(String subject) {
                this.subject = subject;
            }

            public String getSubject() {
                return subject;
            }
        }

        private String getShortName(String key) {
            final String[] keyParts = key.split("\\.");
            return keyParts[keyParts.length - 1];
        }
    }

}

